import jwt from '../../common/jwt'
import {
  ERROR
} from '../../common/error'
import {
  dbCmd,
  userCollection,
  roleCollection
} from '../../common/constants'
import {
  getDistinctArray,
  compareUniIdVersion
} from '../../common/utils'
import {
  version
} from '../../../package.json'

export default class TokenUtils {
  constructor ({
    uniId
  } = {}) {
    /**
     * createToken、checkToken、refreshToken均有uid
     */
    this.uid = null
    /**
     * createToken、refreshToken均有userRecord，checkToken在刷新token时有userRecord
     */
    this.userRecord = null
    this.userPermission = null
    this.oldToken = null
    this.oldTokenPayload = null
    this.uniId = uniId
    this.config = this.uniId._getConfig()
    this.clientInfo = this.uniId._clientInfo
    this.checkConfig()
  }

  checkConfig () {
    const {
      tokenExpiresIn,
      tokenExpiresThreshold
    } = this.config
    if (tokenExpiresThreshold >= tokenExpiresIn) {
      throw new Error('Config error, tokenExpiresThreshold should be less than tokenExpiresIn')
    }
    if (tokenExpiresThreshold > tokenExpiresIn / 2) {
      console.warn(`Please check whether the tokenExpiresThreshold configuration is set too large, tokenExpiresThreshold: ${tokenExpiresThreshold}, tokenExpiresIn: ${tokenExpiresIn}`)
    }
  }

  get customToken () {
    return this.uniId.interceptorMap.get('customToken')
  }

  isTokenInDb (tokenVersion) {
    /**
     * uni-id-common 1.0.10以上版本需要在可能的情况下校验数据库内的token
     */
    return compareUniIdVersion(tokenVersion, '1.0.10') >= 0
  }

  async getUserRecord () {
    if (this.userRecord) {
      return this.userRecord
    }
    const getUserRes = await userCollection.doc(this.uid).get()
    this.userRecord = getUserRes.data[0]
    if (!this.userRecord) {
      throw {
        errCode: ERROR.ACCOUNT_NOT_EXISTS
      }
    }
    switch (this.userRecord.status) {
      case undefined:
      case 0:
        break
      case 1:
        throw {
          errCode: ERROR.ACCOUNT_BANNED
        }
      case 2:
        throw {
          errCode: ERROR.ACCOUNT_AUDITING
        }
      case 3:
        throw {
          errCode: ERROR.ACCOUNT_AUDIT_FAILED
        }
      case 4:
        throw {
          errCode: ERROR.ACCOUNT_CLOSED
        }
      default:
        break
    }

    // refreshToken、checkToken时如果用到userRecord就会走此逻辑
    if (this.oldTokenPayload) {
      const isTokenInDb = this.isTokenInDb(this.oldTokenPayload.uniIdVersion)
      // uni-id-common 1.0.10起重新启用token存储于数据库
      if (isTokenInDb) {
        const token = this.userRecord.token || []
        if (token.indexOf(this.oldToken) === -1) {
          throw {
            errCode: ERROR.CHECK_TOKEN_FAILED
          }
        }
      }
      // valid_token_date再用户更新密码时会进行更新，目的是让所有token失效
      if (this.userRecord.valid_token_date && this.userRecord.valid_token_date > this.oldTokenPayload.iat * 1000) {
        throw {
          errCode: ERROR.TOKEN_EXPIRED
        }
      }
    }
    return this.userRecord
  }

  async updateUserRecord (data) {
    await userCollection.doc(this.uid).update(data)
  }

  async getUserPermission () {
    if (this.userPermission) {
      return this.userPermission
    }
    const userRecord = await this.getUserRecord()
    const role = userRecord.role || []
    if (role.length === 0) {
      this.userPermission = {
        role: [],
        permission: []
      }
      return this.userPermission
    }
    if (role.includes('admin')) {
      this.userPermission = {
        role,
        permission: []
      }
      return this.userPermission
    }
    const getRoleListRes = await roleCollection.where({
      role_id: dbCmd.in(role)
    }).get()
    const permission = getDistinctArray(
      getRoleListRes.data.reduce((list, item) => {
        if (item.permission) {
          list.push(...item.permission)
        }
        return list
      }, [])
    )
    this.userPermission = {
      role,
      permission
    }
    return this.userPermission
  }

  /**
   * 创建token
   * @param {Object}  param
   * @param {String}  param.uid         用户id，必填
   * @param {Array}   param.role        用户角色，非必填
   * @param {Array}   param.permission  用户权限，非必填
   */
  async _createToken ({
    uid,
    role,
    permission
  } = {}) {
    if (!role || !permission) {
      const getUserPermissionResult = await this.getUserPermission()
      role = getUserPermissionResult.role
      permission = getUserPermissionResult.permission
    }
    let signContent = {
      uid,
      role,
      permission
    }
    if (this.uniId.interceptorMap.has('customToken')) {
      const customToken = this.uniId.interceptorMap.get('customToken')
      if (typeof customToken !== 'function') {
        throw new Error('Invalid custom token file')
      }
      signContent = await customToken({
        uid,
        role,
        permission
      })
    }

    const now = Date.now()
    const {
      tokenSecret,
      tokenExpiresIn,
      maxTokenLength = 10
    } = this.config
    const token = jwt.sign({
      ...signContent,
      uniIdVersion: version
    }, tokenSecret, {
      expiresIn: tokenExpiresIn
    })
    const userRecord = await this.getUserRecord()

    const tokenList = (userRecord.token || []).filter(item => {
      try {
        const payload = this._checkToken(item)
        if (userRecord.valid_token_date && userRecord.valid_token_date > payload.iat * 1000) {
          return false
        }
      } catch (error) {
        if (error.errCode === ERROR.TOKEN_EXPIRED) {
          return false
        }
      }
      return true
    })

    tokenList.push(token)

    if (tokenList.length > maxTokenLength) {
      tokenList.splice(0, tokenList.length - maxTokenLength)
    }

    await this.updateUserRecord({
      last_login_ip: this.clientInfo.clientIP,
      last_login_date: now,
      token: tokenList
    })
    return {
      token,
      tokenExpired: now + tokenExpiresIn * 1000
    }
  }

  /**
   * 创建token
   * @param {Object}  param
   * @param {String}  param.uid         用户id，必填
   * @param {Array}   param.role        用户角色，非必填
   * @param {Array}   param.permission  用户权限，非必填
   */
  async createToken ({
    uid,
    role,
    permission
  } = {}) {
    if (!uid) {
      throw {
        errCode: ERROR.PARAM_REQUIRED,
        errMsgValue: {
          param: 'uid'
        }
      }
    }
    this.uid = uid
    const {
      token,
      tokenExpired
    } = await this._createToken({
      uid,
      role,
      permission
    })
    return {
      errCode: 0,
      token,
      tokenExpired
    }
  }

  /**
   * 刷新token
   * @param {Object} param
   * @param {String} param.token 旧token
   */
  async refreshToken ({
    token
  } = {}) {
    if (!token) {
      throw {
        errCode: ERROR.PARAM_REQUIRED,
        errMsgValue: {
          param: 'token'
        }
      }
    }

    this.oldToken = token
    const payload = this._checkToken(token)
    this.uid = payload.uid
    this.oldTokenPayload = payload

    const {
      uid
    } = payload
    const {
      role,
      permission
    } = await this.getUserPermission()
    const {
      token: newToken,
      tokenExpired
    } = await this._createToken({
      uid,
      role,
      permission
    })
    return {
      errCode: 0,
      token: newToken,
      tokenExpired
    }
  }

  /**
   * 内部checkToken方法
   * @param {String} token token内容
   */
  _checkToken (token) {
    const {
      tokenSecret
    } = this.config
    let payload
    try {
      payload = jwt.verify(token, tokenSecret)
    } catch (error) {
      if (error.name === 'TokenExpiredError') {
        throw {
          errCode: ERROR.TOKEN_EXPIRED
        }
      }
      throw {
        errCode: ERROR.CHECK_TOKEN_FAILED
      }
    }
    return payload
  }

  /**
   * 校验token
   * @param {String}  token             token
   * @param {Object}  param
   * @param {Boolean} param.autoRefresh 是否自动刷新，默认自动刷新
   */
  async checkToken (token, {
    autoRefresh = true
  } = {}) {
    if (!token) {
      throw {
        errCode: ERROR.CHECK_TOKEN_FAILED
      }
    }
    this.oldToken = token
    const payload = this._checkToken(token)
    this.uid = payload.uid
    this.oldTokenPayload = payload

    const {
      tokenExpiresThreshold
    } = this.config
    const {
      uid,
      role,
      permission
    } = payload

    const rbacInfo = {
      role,
      permission
    }
    if (!role && !permission) {
      const {
        role: userRole,
        permission: userPermission
      } = await this.getUserPermission()
      rbacInfo.role = userRole
      rbacInfo.permission = userPermission
    }
    if (!tokenExpiresThreshold || !autoRefresh) {
      const result = {
        code: 0,
        errCode: 0,
        ...payload,
        ...rbacInfo
      }
      delete result.uniIdVersion
      return result
    }
    const now = Date.now()
    const needRefreshToken = payload.exp * 1000 - now < tokenExpiresThreshold * 1000
    let newToken = {}
    if (needRefreshToken) {
      newToken = await this._createToken({
        uid
      })
    }

    const result = {
      code: 0,
      errCode: 0,
      ...payload,
      ...rbacInfo,
      ...newToken
    }
    delete result.uniIdVersion
    return result
  }
}
